#region License
//-----------------------------------------------------------------------------
// Copyright (c) 2008, Aaron MacDougall, Daniel Jeffery
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
//   this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright notice,
//   this list of conditions and the following disclaimer in the documentation
//   and/or other materials provided with the distribution.
//
// * Neither the name of Aaron MacDougall or Daniel Jeffery nor the names of its contributors may
//   be used to endorse or promote products derived from this software without
//   specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//-----------------------------------------------------------------------------
#endregion

#region Using Statements
using System;
using System.Diagnostics;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
#endregion

namespace Agro2D
{
    /// <summary>
    /// Collision groups are used to determine which Sprite objects can
    /// collide. 31 groups is the maximum possible with a .Net integer.
    /// </summary>
    [Flags]
    public enum CollisionGroups : uint
    {
        None = 0,
        All = Group31,
        Group1 = 1,
        Group2 = 2,
        Group3 = 4,
        Group4 = 8,
        Group5 = 16,
        Group6 = 32,
        Group7 = 64,
        Group8 = 128,
        Group9 = 256,
        Group10 = 512,
        Group11 = 1024,
        Group12 = 2048,
        Group13 = 4096,
        Group14 = 8192,
        Group15 = 16384,
        Group16 = 32768,
        Group17 = 65536,
        Group18 = 131072,
        Group19 = 262144,
        Group20 = 524288,
        Group21 = 1048576,
        Group22 = 2097152,
        Group23 = 4194304,
        Group24 = 8388608,
        Group25 = 16777216,
        Group26 = 33554432,
        Group27 = 67108864,
        Group28 = 134217728,
        Group29 = 268435456,
        Group30 = 536870912,
        Group31 = 1073741824
    }

    /// <summary>
    /// A singleton class that stores all Sprite collision data, and handles
    /// collision detection.
    /// 
    /// For each Texture2D used for per-pixel collision detection, you must
    /// pre-load collision data by calling CreateCollisionData().
    /// </summary>
    public sealed class CollisionManager
    {
        /// <summary>
        /// A container for a 2D array of boolean filled pixel values.
        /// </summary>
        public class CollisionData
        {
            public bool[,] PixelFlags;
        }

        #region Fields
        private static readonly CollisionManager instance = new CollisionManager();
        private Dictionary<TextureReference, CollisionData> collisionData;
        private Dictionary<string, CollisionGroups> collisionGroupIDs;
        #endregion

        #region Properties
        /// <summary>
        /// Retrieves the singleton instance of this class.
        /// </summary>
        public static CollisionManager Instance
        {
            get { return instance; }
        }
        #endregion

        #region Constructors/Destructors
        /// <summary>
        /// Constructor is private because this is a singleton class.
        /// </summary>
        private CollisionManager()
        {
            this.collisionData = new Dictionary<TextureReference, CollisionData>();
            this.collisionGroupIDs = new Dictionary<string, CollisionGroups>();
        }
        #endregion

        /// <summary>
        /// Creates collision data from a supplied texture, to be used for
        /// per-pixel collision detection.
        /// </summary>
        /// <param name="texture">
        /// Texture to create collision data from.
        /// </param>
        public void CreateCollisionData(TextureReference texture)
        {
            Debug.Assert(texture != null);

            CreateCollisionDataInternal(texture);
        }

        /// <summary>
        /// Creates collision data from a supplied texture, to be used for
        /// per-pixel collision detection.
        /// </summary>
        /// <param name="texture">
        /// Texture to create collision data from.
        /// </param>
        /// <returns>New collision data.</returns>
        private CollisionData CreateCollisionDataInternal(TextureReference textureReference)
        {
            // Check if collision data already exists
            if (this.collisionData.ContainsKey(textureReference))
                return this.collisionData[textureReference];

            Texture2D texture = TextureManager.Instance.GetTexture(textureReference);
            CollisionData collisionData = new CollisionData();
            collisionData.PixelFlags = new bool[texture.Width, texture.Height];

            int length = texture.Width * texture.Height;
            Color[] textureData = new Color[length];
            texture.GetData<Color>(textureData);

            int x = 0;
            int y = 0;

            // Go through each pixel setting a boolean flag if not clear
            for (int i = 0; i < length; i++)
            {
                if (textureData[i].A != 0)
                    collisionData.PixelFlags[x, y] = true;

                x++;
                if (x == texture.Width)
                {
                    x = 0;
                    y++;
                }
            }

            this.collisionData.Add(textureReference, collisionData);

            return collisionData;
        }

        /// <summary>
        /// Retrieves the collision data for the supplied texture.
        /// </summary>
        /// <param name="texture">Texture key for collision data.</param>
        /// <returns>Collision data for the supplied texture.</returns>
        public CollisionData GetCollisionData(TextureReference texture)
        {
            Debug.Assert(texture != null);
            Debug.Assert(
                this.collisionData.ContainsKey(texture),
                "Collision data has not been created. Use CollisionManager.CreateCollisionData()."
                );

            return this.collisionData[texture];
        }

        /// <summary>
        /// Clears all collision data. Should be used at the end of a level,
        /// before the new collision data is created.
        /// </summary>
        public void Clear()
        {
            this.collisionData.Clear();
        }

        /// <summary>
        /// Checks whether a collision occurred between two Sprite objects.
        /// </summary>
        /// <param name="sprite1">First Sprite.</param>
        /// <param name="sprite2">Second Sprite.</param>
        /// <returns>true is a collision took place, otherwise false.</returns>
        CollisionProperties sprite1CollProps;
        CollisionProperties sprite2CollProps;
        SpriteProperties sprite1SpriteProps;
        SpriteProperties sprite2SpriteProps;
        Vector2 sprite1Position;
        Vector2 sprite2Position;
        Rectangle rect1;
        Rectangle rect2;
        public bool CheckCollision(Sprite sprite1, Sprite sprite2)
        {
            sprite1CollProps = sprite1.CollisionProperties;
            sprite2CollProps = sprite2.CollisionProperties;
            sprite1SpriteProps = sprite1.SpriteProperties;
            sprite2SpriteProps = sprite2.SpriteProperties;

            // Check if collision detection is necessary
            if (!(sprite1CollProps.CollisionType == CollisionType.None ||
                sprite2CollProps.CollisionType == CollisionType.None))
            {
                rect1 = sprite1CollProps.BoundingBox;
                rect2 = sprite2CollProps.BoundingBox;

                // Use SourceRectangle as bounding box is none is defined
                if (rect1.Width == 0 && rect1.Height == 0)
                {
                    rect1 = new Rectangle(
                        0,
                        0,
                        sprite1SpriteProps.SourceRectangle.Width,
                        sprite1SpriteProps.SourceRectangle.Height
                        );
                }
                if (rect2.Width == 0 && rect2.Height == 0)
                {
                    rect2 = new Rectangle(
                        0,
                        0,
                        sprite2SpriteProps.SourceRectangle.Width,
                        sprite2SpriteProps.SourceRectangle.Height
                        );
                }

                // Update collision rectangles
                sprite1Position = sprite1SpriteProps.Position;
                sprite2Position = sprite2SpriteProps.Position;

                rect1.X += (int)sprite1Position.X;
                rect1.Y += (int)sprite1Position.Y;
                rect2.X += (int)sprite2Position.X;
                rect2.Y += (int)sprite2Position.Y;

                // Check bounding box collision
                bool rectCollision = false;
                rect1.Intersects(ref rect2, out rectCollision);
                if (rectCollision)
                {
                    bool perPixel1 = false;
                    bool perPixel2 = false;

                    if (sprite1CollProps.CollisionType == CollisionType.PerPixel &&
                        sprite1SpriteProps.Texture != null
                        )
                        perPixel1 = true;
                    if (sprite2CollProps.CollisionType == CollisionType.PerPixel &&
                        sprite2SpriteProps.Texture != null
                        )
                        perPixel2 = true;

                    // Check if per-pixel collision detection is required
                    if (perPixel1 || perPixel2)
                    {
                        // Only check the common overlapping areas
                        int top = Math.Max(rect1.Top, rect2.Top);
                        int bottom = Math.Min(rect1.Bottom, rect2.Bottom);
                        int left = Math.Max(rect1.Left, rect2.Left);
                        int right = Math.Min(rect1.Right, rect2.Right);

                        CollisionData collision1 = null;
                        CollisionData collision2 = null;

                        if (perPixel1)
                        {
                            collision1 = GetCollisionData(sprite1SpriteProps.Texture);
                            // Offset to the correct frame
                            rect1.X -= sprite1SpriteProps.SourceRectangle.X;
                            rect1.Y -= sprite1SpriteProps.SourceRectangle.Y;
                        }
                        if (perPixel2)
                        {
                            collision2 = GetCollisionData(sprite2SpriteProps.Texture);
                            // Offset to the correct frame
                            rect2.X -= sprite2SpriteProps.SourceRectangle.X;
                            rect2.Y -= sprite2SpriteProps.SourceRectangle.Y;
                        }

                        bool filled1;
                        bool filled2;

                        // Check every point within the intersection bounds
                        for (int y = top; y < bottom; y++)
                        {
                            for (int x = left; x < right; x++)
                            {
                                filled1 = false;
                                filled2 = false;

                                if (perPixel1)
                                    filled1 = collision1.PixelFlags[x - rect1.Left, y - rect1.Top];
                                else
                                    filled1 = true;

                                if (perPixel2)
                                    filled2 = collision2.PixelFlags[x - rect2.Left, y - rect2.Top];
                                else
                                    filled2 = true;

                                if (filled1 && filled2)
                                    return true;
                            }
                        }
                    }
                    else
                        return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Adds a string representation of a collision group.
        /// </summary>
        /// <param name="id">Name of the group.</param>
        /// <param name="groups">Group value.</param>
        public void AddCollisionGroupID(string id, CollisionGroups groups)
        {
            Debug.Assert(!this.collisionGroupIDs.ContainsKey(id), "Collision Group ID has already been added.");

            this.collisionGroupIDs.Add(id, groups);
        }

        /// <summary>
        /// Clears currently loaded IDs
        /// </summary>
        public void ClearCollisionGroupIDs()
        {
            this.collisionGroupIDs.Clear();
        }

        /// <summary>
        /// Retrives the collision group for the supplied string ID.
        /// </summary>
        /// <param name="id">Name of the group</param>
        /// <returns>Collision group.</returns>
        public CollisionGroups GetCollisionGroupID(string id)
        {
            Debug.Assert(
                this.collisionGroupIDs.ContainsKey(id),
                "Collision ID has not been created. Use CollisionManager.AddCollisionGroupID()."
                );

            return collisionGroupIDs[id];
        }

        /// <summary>
        /// Retrives the collision group for the supplied string ID.
        /// </summary>
        /// <param name="id">Name of the group</param>
        /// <returns>Collision group.</returns>
        public Dictionary<string, CollisionGroups> GetCollisionGroupIDs()
        {
            return collisionGroupIDs;
        }
    }
}